package lab1;

import java.io.*;
import java.util.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class CommunicationThread extends Thread {
    private final Socket clientSocket;// 需要通信的客户端的套接字
    private Socket proxySocket;// 本代理服务器创建的访问目的网站服务器的套接字
    private String requestGram = "";// 客户端的请求报文
    private String targetUrl;// 请求报文头部行中的请求访问的rul
    private String targetHost;// 请求报文头部行中的请求访问的host
    private final HttpProxy httpProxy;//主线程的类，用于获取各种过滤等信息

    public CommunicationThread(Socket clientSocket, HttpProxy httpProxy) {
        this.clientSocket = clientSocket;
        this.httpProxy = httpProxy;
    }

    /**
     * 初始化targetHost和targetPort
     *
     * @param head 头部行
     */
    private void parseRequest(String head) {
        targetUrl = head.split(" ")[1];
        try{
            targetHost = targetUrl.split("/")[2];
        }
        catch (Exception e){
            System.out.println("解析出错！");
        }
        finally {
            System.out.println("正在请求访问：" + targetUrl);
        }
    }

    /**
     * 用于用户过滤、网站过滤、网站钓鱼引导
     */
    public boolean filterAndFishing() throws IOException {
        if (httpProxy.getUserFilter().contains(clientSocket.getInetAddress().getHostAddress())) {
            System.out.println("用户受限:\t" + clientSocket.getInetAddress().getHostAddress());
            return false;
        } else if (httpProxy.getWebFilter().contains(targetHost)) {
            System.out.println("目的网站的访问受限:\t" + targetHost);
            return false;
        } else if (httpProxy.getFishing().containsKey(targetHost)) {
            String oldTargetHost = targetHost;
            targetHost = httpProxy.getFishing().get(targetHost);
            targetUrl = targetUrl.replace(oldTargetHost, targetHost);
            requestGram = requestGram.replace(oldTargetHost, targetHost);// 将请求报文中的头部url替换为引导网站的URL
            System.out.println("网站引导:\t" + oldTargetHost + " >>> " + targetHost);
            return true;
        }
        return true;
    }

    /**
     * 判断是否有缓存文件并且执行代理操作（需要加锁，防止多线程时读写文件错误）
     *
     *
     * @throws IOException IO异常
     */
    public synchronized void cache() throws IOException {
        String cacheFileBase = "./src/lab1/cacheFile/";
        if (!new File(cacheFileBase + this.targetHost).exists()) {
            if (!new File(cacheFileBase + this.targetHost).mkdir()) {
                System.out.println("创建文件夹失败！");
            }
        }
        int urlShortcut = targetUrl.hashCode(); //用hashCode来代替url地址
        File cacheFile = new File(cacheFileBase + targetHost + "/" + urlShortcut + ".txt");
        PrintWriter targetSeverOut = new PrintWriter(proxySocket.getOutputStream());// 向目的网站服务器发送的流
        InputStream targetSeverIn = proxySocket.getInputStream();// 目的网站服务器向代理服务器端返回响应的流
        OutputStream clientOut = clientSocket.getOutputStream();// 代理服务器向客户端发送的流
        if (!cacheFile.exists()) {// 若对应的缓存文件不存在，则创建该文件用于记录新返回的响应报文。如果超出缓存大小，则根据缓存时间，删除最先缓存的文件
            String deleteFile = httpProxy.checkCacheMemoryLeft(cacheFileBase);
            if (deleteFile!=null){
                System.out.println("缓存已满，此文件被删除\t" + deleteFile);
            }
            System.out.println("缓存文件不存在，需转发请求。文件名:" + urlShortcut + ".txt");
            FileOutputStream cacheFileOut = new FileOutputStream(cacheFile);// 写缓存文件的流
            targetSeverOut.write(requestGram); // 向服务器转发原请求
            targetSeverOut.flush();
            while (true) {
                try {
                    proxySocket.setSoTimeout(httpProxy.getSocketTimeOut());// 设置超时时间用于跳出阻塞状态
                    int b = targetSeverIn.read();// 字节流读取响应报文
                    if (b == -1) {
                        break;
                    } else {
                        cacheFileOut.write(b);// 写入到缓存文件中
                        clientOut.write(b);// 写入到向客户端发送响应的流
                        proxySocket.setSoTimeout(0);
                    }
                } catch (SocketTimeoutException e) {
                    break;
                }
            }
            System.out.println("响应报文来源:目的服务器\t新建缓存文件名:" + urlShortcut + ".txt");
            cacheFileOut.close();
        } else {// 文件已经存在，则需要判断：若文件需要更新，则更新并且重新写入缓存文件；否则直接将缓存作为响应
            DateFormat df = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH);
            df.setTimeZone(TimeZone.getTimeZone("GMT"));
            this.requestGram = this.requestGram.replace("\r\n\r\n",
                    "\r\nIf-Modified-Since: " + df.format(cacheFile.lastModified()) + "\r\n\r\n");
            targetSeverOut.write(requestGram);// 发送构造的新的请求报文，添加了文件的最后修改时间
            targetSeverOut.flush();
            // 接收服务器的请求
            List<Byte> outBytes = new ArrayList<>();
            while (true) {
                try {
                    proxySocket.setSoTimeout(httpProxy.getSocketTimeOut());// 设置超时时间用于跳出流阻塞
                    int b = targetSeverIn.read();// 读取响应报文
                    if (b == -1) {
                        break;
                    } else {
                        outBytes.add((byte) (b));
                        proxySocket.setSoTimeout(0);
                    }
                } catch (SocketTimeoutException e) {
                    break;
                }
            }
            // 用于储存接受的响应报文字节流信息
            byte[] responseByte = new byte[outBytes.size()];
            int count = 0;// 用于构造响应字节报文
            for (Byte b : outBytes) {
                responseByte[count++] = b;
            }
            // 接受来自服务器的响应报文
            String responseGram = new String(responseByte);// 构造文本响应报文
            if (responseGram.split("\r\n")[0].contains("304")) {// 响应报文头含304，则缓存可用
                httpProxy.upDateHit();
                System.out.println("缓存命中数: " + httpProxy.getHit() + "\t命中: " + this.targetUrl
                        + "\t文件名: " + urlShortcut);
                FileInputStream cacheFileRead = new FileInputStream(cacheFile);
                int b;// 直接将缓存报文发送给客户端
                while ((b = cacheFileRead.read()) != -1) {
                    clientOut.write(b);// 写入客户端的流
                }
                cacheFileRead.close();
                System.out.println("响应报文来源:缓存文件端\t且不进行修改" + urlShortcut);
            } else if (responseGram.split("\r\n")[0].contains("200")) {// 响应报文头含200，更新缓存
                System.out.println("缓存文件存在但需更新，文件名:" + urlShortcut);
                FileOutputStream cacheFileOut = new FileOutputStream(cacheFile);// 写缓存文件的流
                clientOut.write(responseByte);// 将从服务器读取到的转发给客户端
                cacheFileOut.write(responseByte);// 更新本地缓存
                cacheFileOut.close();
                System.out.println("响应报文来源:目的服务器端\t且进行了修改" + urlShortcut);
            }
        }
    }

    /**
     * 实现代理服务器的缓存、转发、过滤、引导
     */
    @Override
    public void run() {
        // 默认与服务器的连接端口为80
        int targetPort = 80;
        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));// 用于读取客户端发出的请求报文
            String proxyLine = br.readLine();
            if (proxyLine == null) {
                return;
            }
            parseRequest(proxyLine);// 解析头部行，并设置目的主机的url地址以及主机号
            while (proxyLine != null) {
                try {
                    requestGram += proxyLine + "\r\n";// 获取请求报文的信息
                    clientSocket.setSoTimeout(httpProxy.getSocketTimeOut());// 设置超时时间，用于跳出流的阻塞状态
                    proxyLine = br.readLine();
                    clientSocket.setSoTimeout(0);
                } catch (SocketTimeoutException e) {
                    break;
                }
            }
            if (!filterAndFishing()) {// 网站过滤，用户过滤，钓鱼
                return;
            }
            proxySocket = new Socket(targetHost, targetPort);// 建立与服务器通信的套接字
            cache();// 判断请求报文是否可以由缓存文件给出，并进行相应的操作
            proxySocket.close();
            clientSocket.close();
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
